![]() |
![]() |
|
Ein Client, der diese Methode asynchron ausführen möchte, kann einen Delegaten deklarieren und diesem die Adresse der Methode AsyncTestProc übergeben:
Das reicht bereits aus, um AsyncTestProc in einem separaten Thread abzuarbeiten. Dem Aufruf von BeginInvoke müssen Argumente übergeben werden, die unsere Anweisung noch nicht enthält. Sehen wir uns deshalb nun die Definition von BeginInvoke an.
Aufgerufen wird BeginInvoke auf die Instanz eines Delegaten, der auf eine bestimmte Methode zeigt. Weist die aufzurufende Methode eine Parameterliste auf, müssen die erforderlichen Argumente von BeginInvoke an die Methode weitergeleitet werden. Dazu dient die optionale Parameterliste. Theoretisch wäre das bereits vollkommen ausreichend, um die aufgerufene Methode asynchron auszuführen. Meistens benötigt der Aufrufer aber Kenntnis von der Beendigung der asynchronen Ausführung, beispielsweise, wenn er die Rückgabewerte verarbeitet. Folglich muss es eine Möglichkeit geben, die es der asynchron aufgerufenen Methode ermöglicht, den Aufrufer davon zu unterrichten, dass sie ihre Operationen beendet hat. Dabei kann es sich nur um den Aufruf einer Methode im Initiator der asynchronen Operation handeln. Konsequenterweise muss der asynchron aufgerufenen Methode die Adresse der Rückrufmethode im Aufrufer bekannt sein. Das klingt wieder verdächtig nach einem Delegaten – und tatsächlich ist dem so, denn dem Aufruf von BeginInvoke werden nicht nur die Argumente übergeben, welche die asynchron aufgerufene Methode benötigt, sondern darüber hinaus auch ein Objekt vom Typ AsyncCallback, bei dem es sich um den erforderlichen Delegaten handelt. Die Definition des Delegaten AsyncCallback lautet:
Die Methode, die aus der asynchron ausgeführten zurückgerufen wird, hat keinen Rückgabewert und muss einen Parameter vom Typ IAsyncResult definieren. BeginInvoke verfügt noch über einen weiteren Parameter vom Typ Object. Hier kann beim Start der asynchronen Operation ein beliebiges Objekt übergeben werden, das Informationen beliebiger Art enthält. Das hört sich komplizierter an, als es tatsächlich ist. Daher wollen wir den Ablauf schrittweise an einem kleinen Beispiel verfolgen. Gegeben seien das Modul Module1 und die Klasse ClassA:
Aus Main heraus soll die Methode AsyncTest in der Klasse ClassA asynchron aufgerufen werden. Diese Forderung bewirkt, dass wir BeginInvoke auf einen Delegaten aufrufen müssen, der die asynchron auszuführende Methode im Objekt vom Typ ClassA beschreibt. Dazu wird zunächst auf Klassenebene der Klasse Program ein Delegat mit
deklariert. Anschließend verschaffen wir uns ein Objekt vom Typ des Delegaten, dem als Argument die asynchron auszuführende Methode übergeben wird.
Mit
wird die asynchrone Ausführung von AsyncTest in einem Hintergrund-Thread gestartet. Allerdings ist die Anweisung noch unvollständig – symbolisiert durch die Punkte. Wir sollten in Module1 nämlich noch eine Methode bereitstellen, über die der Hintergrund-Thread das Objekt über das Ende seiner Operation benachrichtigt. Die Definition der Rückrufmethode muss dem Delegaten AsyncCallback entsprechen, demnach also einen Parameter vom Typ IAsyncResult enthalten. Wir nennen diese Methode MyCallBackProc.
Das Objekt vom Typ IAsyncResult entspricht dem Rückgabewert von BeginInvoke. Es veröffentlicht insgesamt sechs Eigenschaften. Dazu gehört unter anderem auch IsCompleted. Über IsCompleted kann der Aufrufer jederzeit feststellen, ob die asynchrone Ausführung bereits beendet ist. Eine zweite, sehr interessante Eigenschaft ist AsyncState, die genau das Objekt abruft, das als letzter Parameter dem Aufruf von BeginInvoke übergeben worden ist. Sie werden später in einem anderen Beispiel die sinnvolle Auswertung dieses Objekts sehen. Wir wollen nun unser Beispiel komplettieren und sowohl innerhalb des Servers als auch innerhalb des Clients Code einsetzen, der tatsächlich einige Zeit in Anspruch nimmt, damit wir den Effekt des asynchronen Aufrufs tatsächlich beobachten können.
In der Abbildung 11.10 ist das Ergebnis des Aufrufs zu sehen. Es ist eindeutig zu erkennen, dass .P. bzw. .X. mehr oder weniger abwechselnd ausgegeben werden, denn beide Methoden arbeiten parallel. Beendet wird die asynchrone Operation durch den Rückruf von MyCallbackProc, was durch die Ausgabe des bekannten Satzes »Ich habe fertig« bestätigt wird.
Abbildung 11.10 Ausgabe eines asynchronen Aufrufs Beispielprogramm – Pumpen asynchron einschaltenDas Beispielprogramm der Pumpenschaltung aus Kapitel 7 zeigte, wie die Pumpen mittels Rückruf den Client über das Einschalten der Motoren in Kenntnis setzten. In diesem Beispiel wurden die Pumpen der Reihe nach eingeschaltet. Dabei wurde jedoch nicht berücksichtigt, dass der Startvorgang durchaus eine gewisse Zeit in Anspruch nehmen kann. Jetzt soll das Programm so abgeändert werden, dass die Einschaltvorgänge nicht mehr hintereinander, sondern gleichzeitig erfolgen.
In diesem Beispielprogramm werden nicht die Delegaten auf die Startmethoden an die steuernde Klasse übergeben, sondern die Referenzen auf die Pumpen. Damit wir später die Pumpen identifizieren können, die sich bei der Rückrufmethode nach dem erfolgreichen Starten melden, ist die Pumpenklasse um einen Konstruktor und die Eigenschaft Bezeichner ergänzt worden. Die Eigenschaft liefert den Inhalt des Feldes bezeichner zurück. Weil alle Pumpen dieselbe Methode im Client zurückrufen, bietet es sich an, der Steuerklasse den Delegaten auf diese Methode zu übergeben:
Die Referenz auf den übergebenen Delegaten hält das Objekt der Klasse ControlPumps in einer privaten Variablen vor. Nachdem alle Pumpen in der Steuerklasse registriert sind, können sie durch den Aufruf von StartAllPumps im Client gestartet werden. In einer For Each-Schleife werden alle in der internen Auflistung gespeicherten Pumpenobjekte durchlaufen und für jedes Objekt ein Delegat erzeugt, der auf die objektspezifische Startmethode zeigt. Mit
wird anschließend BeginInvoke auf diesem Delegaten aufgerufen. Damit gewährleisten wir, dass die Startmethode der jeweiligen Pumpe in einem eigenen Thread ausgeführt wird. Das Besondere an diesem Aufruf ist, dass wir dem zweiten Parameter ein Zustandsobjekt übergeben – es handelt es dabei um die Zeichenfolge, die eine Pumpe eindeutig beschreibt. In der Rückrufmethode des Clients kann das dem BeginInvoke-Aufruf übergebene Zustandsobjekt, das von der Steuerklasse an den Thread weitergereicht und von der Rückrufmethode übergeben wird, ausgewertet werden. Dazu dient die Eigenschaft AsyncState des IAsyncResult-Parameters. Damit wissen wir nicht nur, dass eine Pumpe angelaufen ist, sondern gleichzeitig auch, um welche es sich handelt.
11.4.3 Asynchroner Aufruf mit Rückgabewerten
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Public Function EndInvoke([Parameterliste,] IAsyncResult) As Datentyp |
Wie bei BeginInvoke müssen Sie auch EndInvoke eine vorgeschriebene Parameterliste übergeben, die nicht identisch mit der Parameterliste von BeginInvoke ist: Sie darf nur die Referenzparameter der asynchronen Methode enthalten, damit diese ihre Resultate dort hineinschreiben kann. Die Angabe der Werteparameter ist nicht erlaubt. Der einzige grundsätzlich immer zwingend erforderliche Parameter ist vom Typ IAsyncResult. Hier wird das Objekt übergeben, das die Rückrufmethode des Clients vom Server erhalten hat.
Wir wollen nun das Beispiel AsynchronerAufruf_1 ändern, um zu sehen, wie eine asynchrone Methode behandelt wird, die sowohl Werte- als auch Referenzparameter erwartet und darüber hinaus auch noch einen Rückgabewert hat. Dazu implementieren wir die Methode AsyncTest wie folgt:
| Public Function AsyncTest(ByVal x As Integer, _ |
| ByRef y As Long) As String |
| ' zeitaufwändige Ausführung |
| Dim i As Integer |
| For i = 0 To 30 |
| Console.Write(".X.") |
| Thread.Sleep(10) |
| Next |
| y = 12345 |
| Return "Ich habe fertig." |
| End Function |
Die Parameterliste enthält jetzt den Referenzparameter y und den Werteparameter x, außerdem liefert die Methode eine Zeichenfolge zurück.
Die Änderung der Signatur hat natürlich auch im auslösenden Thread Konsequenzen. Der Delegat, der den Aufruf der Methode kapselt, muss an die veränderten Bedingungen angepasst werden:
| Public Delegate Function MyDelegate(ByVal x As Integer, _ |
| ByRef y As Long) As String |
Gleiches gilt auch für den Start der asynchronen Bearbeitung, denn nun reicht es nicht mehr aus, mit BeginInvoke einfach nur einen Delegaten auf die Rückrufmethode zu übergeben sowie die Referenz auf ein Objekt, in das der asynchrone Aufruf Informationen schreiben könnte. Wir müssen stattdessen auch die Parameter der asynchronen Methode in der richtigen Reihenfolge bedienen:
| del.BeginInvoke(intVar, lngVar, callback, Nothing) |
AsyncTest nimmt nun eine Kopie des Integer-Wertes und die Adresse des Long-Wertes entgegen, kann mit diesen die erforderlichen Operationen ausführen und zum Abschluss durch Aufruf der über callback bekannt gegebenen Adresse die Methode MyCallbackProc informieren.
Der Implementierung der Rückrufmethode kommt nun eine entscheidende Bedeutung zu. Es gilt, sowohl den Rückgabewert als auch den in diesem Fall geänderten Inhalt der Variablen lngVar auszuwerten. Dem Aufruf von EndInvoke übergeben wir die Adresse von lngVar und holen uns den Rückgabewert an der Konsole ab:
| Public Sub MyCallbackProc(ByVal ar As IAsyncResult) |
| Dim lngVar As Long |
| ' zeigt den Rückgabewert der asynchronen Methode an |
| Console.Write(del.EndInvoke(lngVar, ar)) |
| ' schreibt den Inhalt des Referenzparameters lngVar |
| Console.Write("..Wert y = {0}", lngVar) |
| End Sub |
Die Konsolenausgabe bestätigt, dass unser Unterfangen von Erfolg beschieden ist: Wir erhalten sowohl die Zeichenfolge als auch den veränderten Inhalt des Feldes intVar.
Zum Abschluss fassen wir das Beispielprogramm noch einmal zusammen.
| ' ---------------------------------------------------------- |
| ' Beispiel: ...\Kapitel 11\AsynchronerAufruf_2 |
| ' ---------------------------------------------------------- |
| Imports System.Threading |
| Module Module1 |
| Public Delegate Function MyDelegate(ByVal x As Integer, _ |
| ByRef y As Long) As String |
| Private del As MyDelegate |
| Sub Main() |
| Dim obj As New ClassA |
| ' Delegat, der die asynchron aufzurufende Methode beschreibt |
| del = New MyDelegate(AddressOf obj.AsyncTest) |
| ' Delegat vom Typ AsyncCallback beschreibt die Methode, die |
| ' der Server nach Beendigung der asynchronen Ausführung aufruft |
| Dim callback As AsyncCallback = _ |
| New AsyncCallback(AddressOf MyCallbackProc) |
| ' die Methode AsyncTest in ClassA asynchron aufrufen |
| Dim intVar As Integer = 34 |
| Dim lngVar As Long = 3000 |
| del.BeginInvoke(intVar, lngVar, callback, Nothing) |
| ' zeitaufwändige Ausführung |
| Dim i As Integer |
| For i = 0 To 100 |
| Console.Write(".P.") |
| Thread.Sleep(10) |
| Next |
| Console.ReadLine() |
| End Sub |
| ' die zurückgerufene Methode |
| Public Sub MyCallbackProc(ByVal ar As IAsyncResult) |
| Dim lngVar As Long |
| ' zeigt den Rückgabewert der asynchronen Methode an |
| Console.Write(del.EndInvoke(lngVar, ar)) |
| ' schreibt den Inhalt des Referenzparameters lngVar |
| Console.Write("..Wert y = {0}", lngVar) |
| End Sub |
| End Module |
| Class ClassA |
| ' asynchron aufzurufende Methode |
| Public Function AsyncTest(ByVal x As Integer, _ |
| ByRef y As Long) As String |
| ' zeitaufwändige Ausführung |
| Dim i As Integer |
| For i = 0 To 30 |
| Console.Write(".X.") |
| Thread.Sleep(10) |
| Next |
| y = 12345 |
| Return "Ich habe fertig." |
| End Function |
| End Class |
Am Anfang dieses Abschnitts wurde schon darauf hingewiesen, dass einige Klassen der .NET-Klassenbibliothek Methoden mit asynchroner Verarbeitung anbieten. Die Klasse FileStream im Namespace System.IO ist ein Beispiel dafür. Es werden allerdings nicht die Methoden BeginInvoke und EndInvoke aufgerufen, sondern zwei ähnlich lautende: BeginRead und EndRead bzw. BeginWrite und EndWrite.
Wir wollen uns nun ansehen, wie eine Klasse aufgebaut ist, die ähnlich wie FileStream implementiert ist. Dabei lernen wir einerseits, wie wir die asynchronen Methoden der Klassen des .NET Frameworks behandeln müssen, andererseits aber auch, diese Technik in eigenen Klassen zu nutzen.
Am Anfang steht die Idee, eine Methode zu entwickeln, von der wir annehmen, dass sie in Abhängigkeit von den Umgebungsbedingungen und der Art der Operation eine längere Zeit zur Bearbeitung in Anspruch nehmen kann. Wir wollen diese Methode nachfolgend Calculate nennen, die Klasse dazu Server.
| Class Server |
| Public Function Calculate(ByVal x As Integer) As Integer |
| Console.Write("---Bearbeitung startet---") |
| Dim i As Integer |
| For i = 0 To 20 |
| Console.Write(".X.") |
| Thread.Sleep(10) |
| Next |
| Console.Write("---Bearbeitung beendet---") |
| Return x * x |
| End Function |
| End Class |
Die For-Schleife simuliert eine länger andauernde Operation. Diese Implementierung arbeitet synchron. Da wir uns bewusst sind, dass Calculate vielleicht auch eine Stunde zur vollständigen Ausführung brauchen könnte (wir sind mit unserer Annahme sehr großzügig), bieten wir zusätzlich eine asynchrone Variante an. Dazu benötigen wir zwei weitere Methoden, die einer allgemeinen Konvention folgend als BeginXxx und EndXxx bezeichnet werden – in unserer Klasse demnach BeginCalculate und EndCalculate. Die noch unvollständige Klassenstruktur sieht dann folgendermaßen aus:
| Class Server |
| ' Methode Calculate wird synchron ausgeführt |
| Public Function Calculate(ByVal x As Integer) As Integer |
| ... |
| End Function |
| ' Start der asynchronen Ausführung |
| Public Function BeginCalculate(ByVal intVar As Integer, _ |
| ByVal asyncCallback As AsyncCallback, _ |
| ByVal state As Object) As IAsyncResult |
| ... |
| End Function |
| ' Beenden der asynchronen Ausführung |
| Public Function EndCalculate(ByVal asyncResult As IAsyncResult) _ |
| As Integer |
| ... |
| End Function |
| End Class |
An dieser Stelle kommt es zu der wichtigsten Entscheidung überhaupt. Was wir beabsichtigen, ist die asynchrone Ausführung der Methode Calculate. Asynchronität heißt aber auch, dass ein weiterer Thread gestartet werden muss, sobald die Methode BeginCalculate aufgerufen wird. Wenn wir in dieser Methode ein Objekt vom Typ Thread erzeugen und seinem Konstruktor einen Delegaten übergeben, bräuchten wir auch noch ein Objekt, welches das Interface IAsyncResult implementiert, müssten zwangsläufig dessen Methoden implementieren usw.
Die Entwicklung auf diese Weise zu gestalten, ist sehr aufwändig. Es gibt eine viel einfachere Lösung, da die beiden Methoden BeginInvoke und EndInvoke genau das leisten, was wir brauchen. Also benutzen wir sie auch, um das Ziel effizient zu erreichen. Dazu wird die Logik, die in den Abschnitten 11.4.2 und 11.4.3 beschrieben wurde, innerhalb der Klasse Server implementiert.
| ' ---------------------------------------------------------- |
| ' Beispiel: ...\ Kapitel 11\AsynchronerAufruf_3 |
| ' ---------------------------------------------------------- |
| Imports System.Threading |
| Class Server |
| ' Deklaration eines Delegaten, der den Funktionsaufruf |
| ' von 'Caluculate' beschreibt |
| Public Delegate Function CalculateHandler(ByVal x As Integer) _ |
| As Integer |
| Dim del As CalculateHandler |
| ' Methode Calculate wird synchron ausgeführt |
| Public Function Calculate(ByVal x As Integer) As Integer |
| Console.Write("---Bearbeitung startet---") |
| Dim i As Integer |
| For i = 0 To 20 |
| Console.Write(".X.") |
| Thread.Sleep(10) |
| Next |
| Console.Write("---Bearbeitung beendet---") |
| Return x * x |
| End Function |
| ' Start der asynchronen Ausführung |
| Public Function BeginCalculate(ByVal intVar As Integer, _ |
| ByVal asyncCallback As AsyncCallback, _ |
| ByVal state As Object) As IAsyncResult |
| del = New CalculateHandler(AddressOf Calculate) |
| ' Aufruf der Methode Calculate, die in einem eigenen |
| ' Thread ausgeführt wird |
| Return del.BeginInvoke(intVar, asyncCallback, Nothing) |
| End Function |
| ' Beenden der asynchronen Ausführung |
| Public Function EndCalculate(ByVal asyncResult As IAsyncResult) _ |
| As Integer |
| Return del.EndInvoke(asyncResult) |
| End Function |
| End Class |
Dem Aufruf der Methode BeginCalculate werden die Daten übergeben, welche die Methode Calculate für ihre Operation benötigt. In unserem Beispiel handelt es sich nur um einen als Werteparameter deklarierten Integer. Der zweite Parameter erhält die Referenz auf einen Delegaten, der die Rückrufmethode im Aufrufer beschreibt. Der dritte und letzte Parameter dient dazu, ein Objekt bereitzustellen, mit dem Daten zwischen dem aufrufenden und dem aufgerufenen Objekt auszutauschen. Ein solches Objekt ist in unserem Beispielcode nicht vorgesehen.
Der Aufruf von BeginCalculate orientiert sich an dem von BeginInvoke – und das ist typisch für Klassen im .NET Framework, die asynchrone Methoden offen legen. Unter ähnlicher Prämisse wird auch EndCalculate implementiert, der Rückgabewert des internen EndInvoke-Aufrufs wird zum Rückgabewert der Instanzmethode.
Es bleibt zum Schluss noch zu testen, ob die Klassenimplementierung auch unseren Anforderungen genügt. Dazu entwickeln wir einen Client mit der Methode Start zum Aufruf der asynchronen Ausführung und einer Methode Results, die als Rückruffunktion vom Server angesteuert wird.
| Class Client |
| Private myObj As New Server |
| Shared Sub Main() |
| Dim myClient As New Client |
| myClient.Start() |
| End Sub |
| Public Sub Start() |
| Dim iVar As Integer = 23 |
| Dim callback As AsyncCallback = _ |
| New AsyncCallback(AddressOf Me.Results) |
| ' Aufruf der asynchronen Ausführung |
| Dim ia As IAsyncResult = _ |
| myObj.BeginCalculate(iVar, callback, Nothing) |
| Dim i As Integer |
| For i = 0 To 100 |
| Console.Write(".{0}.", i) |
| Thread.Sleep(5) |
| Next |
| Console.ReadLine() |
| End Sub |
| ' diese Methode wird vom Server aufgerufen |
| Public Sub Results(ByVal asyncResult As IAsyncResult) |
| ' das Ergebnis der asynchronen Operation abholen |
| Dim res As Integer = myObj.EndCalculate(asyncResult) |
| Console.Write("---Resultat = {0} ", res) |
| Console.Write("---FERTIG---") |
| End Sub |
| End Class |
Die Ausgabe an der Konsole wird wie in der folgenden Abbildung gezeigt aussehen.

Hier klicken, um das Bild zu Vergrößern
Abbildung 11.11 Ausgabe des Beispiels »AsynchronerAufruf_3«
| Hinweis |
|
Sie sollten grundsätzlich immer, wenn die Methode einer Klasse eine länger andauernde Operation ausführt, neben der asynchronen Variante auch die synchron arbeitende Methode anbieten, um dem Benutzer die Entscheidung zu überlassen, ob er die synchrone oder asynchrone Variante aufrufen möchte. |
| << zurück |
|
||||||||||||||
|
||||||||||||||
|
||||||||||||||
|
||||||||||||||
Copyright © Galileo Press 2007
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken.
Ansonsten unterliegt das <openbook> denselben Bestimmungen, wie die
gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich
geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung,
Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.